import React, {Component} from 'react'
import {MarkdownPreview} from 'react-marked-markdown';
// import py1_1 from '../static/py1_1.md'
// import marked from 'marked';

class Py13 extends Component {
    constructor(props) {
        super(props);
        this.state = {
           value: `

           
到目前为止，本书的例子都只是处理单个静态页面，只能算是人为简化的例子（使用作者
的网站页面）。从本章开始，我们会看到一些现实问题，需要用爬虫遍历多个页面甚至多
个网站。

之所以叫网络爬虫（Web crawler）是因为它们可以沿着网络爬行。它们的本质就是一种递
归方式。为了找到 URL 链接，它们必须首先获取网页内容，检查这个页面的内容，再寻
找另一个 URL，然后获取 URL 对应的网页内容，不断循环这一过程。

不过要注意的是：你可以这样重复采集网页，但并不意味着你一直都应该这么做。当你需
要的所有数据都在一个页面上时，前面例子中的爬虫就足以解决问题了。使用网络爬虫的     
时候，你必须非常谨慎地考虑需要消耗多少网络流量，还要尽力思考能不能让采集目标的
服务器负载更低一些。

> 3.1　遍历单个域名

即使你没听说过“维基百科六度分隔理论”，也很可能听过“凯文 · 贝肯（Kevin Bacon）
的六度分隔值游戏”。在这两个游戏中，都是把两个不相干的主题（维基百科里是用词条
之间的连接，凯文 · 贝肯的六度分隔值游戏是用出现在同一部电影中的演员来连接）用一
个总数不超过六条的主题连接起来（包括原来的两个主题）。

比如，埃里克 · 艾德尔和布兰登 · 弗雷泽都出现在电影《骑警杜德雷》里，布兰登 · 弗
雷泽又和凯文 · 贝肯都出现在电影《我呼吸的空气》里。 1 因此，根据这两个条件，从埃里克 · 艾德尔到凯文 ·贝肯的链条主题长度只有 3。

我们将在本节创建一个项目来实现“维基百科六度分隔理论”的查找方法。也就是说，我们
要实现从埃里克 · 艾德尔的词条页面\（https://en.wikipedia.org/wiki/Eric_Idle）开始，经过最
少的链接点击次数找到凯文 · 贝肯的词条页面\（https://en.wikipedia.org/wiki/Kevin_Bacon）。

<center>这么做对维基百科的服务器负载有多大影响 ?</center>


    根据维基媒体基金会（维基百科归属的组织）的统计，网站每秒钟会收到大约 2500次点击，其中超过 99% 的点击都是指向维基百科域名（详情请见“维基媒体统计图”（Wikimedia in Figures）里的“流量数据”（Traffic Volume）部分内容，https://meta.
    wikimedia.org/wiki/Wikimedia_in_figures_-_Wikipedia#Traffic_volume）。因为网站流量很大，所以你的网络爬虫不可能对维基百科的负载有显著影响。不过，如果你频繁地运行本书的代码，或者自己在做项目采集维基百科的词条，那么希望你能够向维基媒体基金会提供一点捐赠（https://wikimediafoundation.org/wiki/Ways_to_Give）——即使是很少的钱来补偿占用的服务器资源，算是帮助维基百科这个教育资源供其他人使用。
   
你应该已经知道如何写一段获取维基百科网站的任何页面并提取页面链接的 Python 代码了：
  
    from urllib.request import urlopen
    from bs4 import BeautifulSoup

    html = urlopen("http://en.wikipedia.org/wiki/Kevin_Bacon")
    bsObj = BeautifulSoup(html)
    for link in bsObj.findAll("a"):
        if 'href' in link.attrs:
            print(link.attrs['href'])
 
如果你观察生成的一列链接，就会看到你想要的所有词条链接都在里面：“Apollo 13”
“Philadelphia”和“Primetime Emmy Award”，等等。但是，也有一些我们不需要的链接

   
    //wikimediafoundation.org/wiki/Privacy_policy
    //en.wikipedia.org/wiki/Wikipedia:Contact_us
    

其实维基百科的每个页面都充满了侧边栏、页眉、页脚链接，以及连接到分类页面、对话
页面和其他不包含词条的页面的链接：

    
    /wiki/Category:Articles_with_unsourced_statements_from_April_2014
    /wiki/Talk:Kevin_Bacon
   

最近我有个朋友在做一个类似维基百科采集这样的项目，他说为了判断维基百科的内链是
否链接到一个词条，他写了一个很大的过滤函数，超过 100 行代码。不幸的是，可能在项
目启动的时候，他没有花时间去比较“词条链接”和“其他链接”的差异，也可能他后来
发现了那个技巧。如果你仔细观察那些指向词条页面（不是指向其他内容页面）的链接，会发现它们都有三个共同点：

* 它们都在 id 是 bodyContent 的 div 标签里
* URL 链接不包含分号
* URL 链接都以 /wiki/ 开头

我们可以利用这些规则稍微调整一下代码来获取词条链接：

    from urllib.request import urlopen
    from bs4 import BeautifulSoup
    import re

    html = urlopen("http://en.wikipedia.org/wiki/Kevin_Bacon")
    bsObj = BeautifulSoup(html)
    for link in bsObj.find("div", {"id":"bodyContent"}).findAll("a",
                                    href=re.compile("^(/wiki/)((?!:).)*$")):
        if 'href' in link.attrs:
            print(link.attrs['href'])
 
如果你运行代码，就会看到维基百科上凯文 ·贝肯词条里所有指向其他词条的链接。

当然，写程序来找出这个静态的维基百科词条里所有的词条链接很有趣，不过没什么实际
用处。我们需要让这段程序更像下面的形式。
* 一个函数 getLinks ，可以用维基百科词条 /wiki/< 词条名称 > 形式的 URL 链接作为参数，
然后以同样的形式返回一个列表，里面包含所有的词条 URL 链接。
* 一个主函数，以某个起始词条为参数调用 getLinks ，再从返回的 URL 列表里随机选择
一个词条链接，再调用 getLinks ，直到我们主动停止，或者在新的页面上没有词条链接
了，程序才停止运行。

完整的代码如下所示：
    
    from urllib.request import urlopen
    from bs4 import BeautifulSoup
    import datetime
    import random
    import re

    random.seed(datetime.datetime.now())
    def getLinks(articleUrl):
        html = urlopen("http://en.wikipedia.org"+articleUrl)
        bsObj = BeautifulSoup(html)
        return bsObj.find("div", {"id":"bodyContent"}).findAll("a",
                                    href=re.compile("^(/wiki/)((?!:).)*$"))
    links = getLinks("/wiki/Kevin_Bacon")
    while len(links) > 0:
    newArticle = links[random.randint(0, len(links)-1)].attrs["href"]
        print(newArticle)
        links = getLinks(newArticle)
 
导入需要的 Python 库之后，程序首先做的是用系统当前时间生成一个随机数生成器。这样
可以保证在每次程序运行的时候，维基百科词条的选择都是一个全新的随机路径。

<center>伪随机数和随机数种子</center>
    
    在前面的示例中，为了能够连续地随机遍历维基百科，我用 Python 的随机数生成器来
    随机选择每一页上的一个词条链接。但是，用随机数的时候需要格外小心。

    虽然计算机很擅长做精确计算，但是它们处理随机事件时非常不靠谱。因此，随机数
    是一个难题。大多数随机数算法都努力创造一种呈均匀分布且难以预测的数据序列，
    但是在算法初始化阶段都需要提供随机数“种子”（random seed）。而完全相同的种子
    每次将产生同样的“随机”数序列，因此我用系统时间作为随机数序列生成的起点。
    这样做会让程序运行的时候更具有随机性。

    其实，Python 的伪随机数（pseudorandom number）生成器用的是梅森旋转（Mersenne
    Twister）算法（https://en.wikipedia.org/wiki/Mersenne_Twister），它产生的随机数很难
    预测且呈均匀分布，就是有点儿耗费 CPU 资源。真正好的随机数可不便宜！
 

然后，我们定义 getLinks 函数，其参数是维基百科词条页面中 /wiki/< 词条名称 > 形式的
URL 链接，前面加上维基百科的域名， http://en.wikipedia.org ，再用域名中的网页获得
一个 BeautifulSoup 对象。之后用前面介绍过的参数抽取一列词条链接所在的标签 a 并返回
它们。

程序的主函数首先把起始页面 https://en.wikipedia.org/wiki/Kevin_Bacon 里的词条链接列表
（ links 变量）设置成链接列表。然后用一个循环，从页面中随机找一个词条链接标签并抽
取 href 属性，打印这个页面链接，再把这个链接传入 getLinks 函数，重新获取新的链接
列表。

当然，这里只是简单地构建一个从一个页面到另一个页面的爬虫，要解决“维基百科六度
分隔理论”问题还有一点儿工作得做。我们还应该存储 URL 链接数据并分析数据。关于
这个问题后续的解决办法，请参考第 5 章内容。

#### 异常处理

虽然为了方便起见，我们在这些示例中忽略了大多数异常处理过程，但是要
注意问题随时可能发生。例如，维基百科改变了 bodyContent 标签的名称怎
么办呢？（提示：那时代码就会崩溃。）

因此，这些脚本作为容易演示的示例也许可以运行得很不错，但是要真正成
为自动化产品代码，还需要增加更多的异常处理。关于异常处理的更多信
息，请参考第 1 章的相关内容。

> 3.2　采集整个网站

在上一节内容里，我们实现了在一个网站上随机地从一个链接跳到另一个链接。但是，如
果你需要系统地把整个网站按目录分类，或者要搜索网站上的每一个页面，怎么办？那就
得采集整个网站，那是一种非常耗费内存资源的过程，尤其是处理大型网站时，最合适的
工具就是用一个数据库来储存采集的资源。但是，我们可以掌握这类工具的行为，并不需
要通过大规模地运行它们。

<center>深网和暗网</center>
    
    你可能听说过深网（deep Web）、暗网（dark Web）或隐藏网络（hidden Web）之类的
    术语，尤其是在最近的媒体中。它们是什么意思呢？

    深网是网络的一部分，与浅网（surface Web）对立。浅网是互联网上搜索引擎可以抓
    到的那部分网络。据不完全统计，互联网中其实约 90% 的网络都是深网。因为谷歌不
    能做像表单提交这类事情，也找不到那些没有直接链接到顶层域名上的网页，或者因
    为有 robots.txt 禁止而不能查看网站，所以浅网的数量相对深网还是比较少的。

    暗网，也被称为 Darknet 或 dark Internet，完全是另一种“怪兽”。它们也建立在已有
    的网络基础上，但是使用 Tor 客户端，带有运行在 HTTP 之上的新协议，提供了一个
    信息交换的安全隧道。这类暗网页面也是可以采集的，就像你采集其他网站一样，不
    过这些内容超出了本书的范围。

    和暗网不同，深网是相对容易采集的。实际上，本书的很多工具都是在教你如何采集
    那些 Google 爬虫机器人不能获取的深网信息。
 

那么，什么时候采集整个网站是有用的，而什么时候采集整个网站又是有害无益的呢？遍
历整个网站的网络数据采集有许多好处。

* 生成网站地图

    几年前，我曾经遇到过一个问题：一个重要的客户想对一个网站的重新设计方案进行效
    果评估，但是不想让我们公司进入他们的网站内容管理系统（CMS），也没有一个公开
    可用的网站地图。我就用爬虫采集了整个网站，收集了所有的链接，再把所有的页面整
    理成他们网站实际的形式。这让我很快找出了网站上以前不曾留意的部分，并准确地计
    算出需要重新设计多少网页，以及可能需要移动多少内容。

* 收集数据

    我的另一个客户为了创建一个专业垂直领域的搜索平台，想收集一些文章（故事、博
    文、新闻等）。虽然这些网站采集并不费劲，但是它们需要爬虫有足够的深度（我们有
    意收集数据的网站不多）。于是我就创建了一个爬虫递归地遍历每个网站，只收集那些
    网站页面上的数据。

一个常用的费时的网站采集方法就是从顶级页面开始（比如主页），然后搜索页面上的所
有链接，形成列表。再去采集这些链接的每一个页面，然后把在每个页面上找到的链接形
成新的列表，重复执行下一轮采集。

很明显，这是一个复杂度增长很快的情形。假如每个页面有 10 个链接，网站上有 5 个页
面深度（一个中等规模网站的主流深度），那么如果你要采集整个网站，一共得采集的网
页数量就是 105，即 100 000 个页面。不过，虽然“5 个页面深度，每页 10 个链接”是网
站的主流配置，但其实很少有网站真的有 100 000 甚至更多的页面。这是因为很大一部分
内链都是重复的。

为了避免一个页面被采集两次，链接去重是非常重要的。在代码运行时，把已发现的所有
链接都放到一起，并保存在方便查询的列表里（下文示例指 Python 的集合 set 类型）。只
有“新”链接才会被采集，之后再从页面中搜索其他链接：
    
    from urllib.request import urlopen
    from bs4 import BeautifulSoup
    import re

    pages = set()
    def getLinks(pageUrl):
        global pages
        html = urlopen("http://en.wikipedia.org"+pageUrl)
        bsObj = BeautifulSoup(html)
        for link in bsObj.findAll("a", href=re.compile("^(/wiki/)")):
            if 'href' in link.attrs:
                if link.attrs['href'] not in pages:
                    # 我们遇到了新页面
                    newPage = link.attrs['href']
                    print(newPage)
                    pages.add(newPage)
                    getLinks(newPage)
    getLinks("")

为了全面地展示这个网络数据采集示例是如何工作的，我降低了在前面例子里使用的“只
寻找内链”的标准。不再限制爬虫采集的页面范围，只要遇到页面就查找所有以 /wiki/ 开
头的链接，也不考虑链接是不是包含分号。（提示：词条链接不包含分号，而文档上传页
面、讨论页面之类的页面 URL 链接都包含分号。）

一开始，用 getLinks 处理一个空 URL，其实是维基百科的主页，因为在函数里空 URL 就
是 http://en.wikipedia.org 。然后，遍历首页上每个链接，并检查是否已经在全局变量
集合 pages 里面了（已经采集的页面集合）。如果不在，就打印到屏幕上，并把链接加入
pages 集合，再用 getLinks 递归地处理这个链接。

#### 关于递归的警告

这个警告在软件开发书籍里很少提到，但是我觉得你应该注意：如果递归运
行的次数非常多，前面的递归程序就很可能崩溃。

Python 默认的递归限制（程序递归地自我调用次数）是 1000 次。因为维基
百科的网络链接浩如烟海，所以这个程序达到递归限制后就会停止，除非你
设置一个较大的递归计数器，或用其他手段不让它停止。

对于那些链接深度少于 1000 的“普通”网站，这个方法通常可以正常运行，
一些奇怪的异常除外。例如，我曾经遇到过一个网站，有一个在生成博文内
链的规则。这个规则是“当前页面把 /blog/title_of_blog.php 加到它后面，作
为本页面的 URL 链接”。

问题是它们可能会把 /blog/title_of_blog.php 加到一个已经有 /blog/ 的 URL 上
面了。因此，网站就多了一个 /blog/。最后，我的爬虫找到了这样的 URL 链
接：/blog/blog/blog/blog.../blog/title_of_blog.php。
后来，我增加了一些条件，对可能导致无限循环的部分进行检查，确保那些
URL 不是这么荒谬。但是，如果你不去检查这些问题，爬虫很快就会崩溃。

### 收集整个网站数据

当然，如果只是从一个页面跳到另一个页面，那么网络爬虫是非常无聊的。为了有效地使
用它们，在用爬虫的时候我们需要在页面上做些事情。让我们看看如何创建一个爬虫来收
集页面标题、正文的第一个段落，以及编辑页面的链接（如果有的话）这些信息。

和往常一样，决定如何做好这些事情的第一步就是先观察网站上的一些页面，然后拟定一
个采集模式。通过观察几个维基百科页面，包括词条和非词条页面，比如隐私策略之类的
页面，就会得出下面的规则

* 所有的标题（所有页面上，不论是词条页面、编辑历史页面还是其他页面）都是在
h1 → span 标签里，而且页面上只有一个 h1 标签。

* 前面提到过，所有的正文文字都在 div#bodyContent 标签里。但是，如果我们想更
进一步获取第一段文字，可能用 div#mw-content-text → p 更好（只选择第一段的标
签）。这个规则对所有页面都适用，除了文件页面（例如，https://en.wikipedia.org/wiki/
File:Orbit_of_274301_Wikipedia.svg），页面不包含内容文字（ content text ）的部分内容。

* 编辑链接只出现在词条页面上。如果有编辑链接，都位于 li#ca-edit 标签的 li#ca-
edit → span → a 里面。

调整前面的代码，我们就可以建立一个爬虫和数据收集（至少是数据打印）的组合程序：
    
    from urllib.request import urlopen
    from bs4 import BeautifulSoup
    import re

    pages = set()
    def getLinks(pageUrl):
        global pages
            html = urlopen("http://en.wikipedia.org"+pageUrl)
            bsObj = BeautifulSoup(html)
            try:
                print(bsObj.h1.get_text())
                print(bsObj.find(id="mw-content-text").findAll("p")[0])
                print(bsObj.find(id="ca-edit").find("span").find("a").attrs['href'])
            except AttributeError:
                print("页面缺少一些属性！不过不用担心！")
            for link in bsObj.findAll("a", href=re.compile("^(/wiki/)")):
                if 'href' in link.attrs:
                    if link.attrs['href'] not in pages:
                        # 我们遇到了新页面
                        newPage = link.attrs['href']
                        print("----------------\n"+newPage)
                        pages.add(newPage)
                        getLinks(newPage)
    getLinks("")

这个 for 循环和原来的采集程序基本上是一样的（除了打印一条虚线来分离不同的页面内
容之外）。

因为我们不可能确保每一页上都有所有类型的数据，所以每个打印语句都是按照数据在页
面上出现的可能性从高到低排列的。也就是说， \<h1>标题标签会出现在每一页上（只要能
识别，无论哪一页都有），所以我们首先试着获取它的数据。正文内容会出现在大多数页
面上（除了文件页面），因此是第二个获取的数据。“编辑”按钮只出现在标题和正文内容
都已经获取的页面上，但不是所有这类页面上都有，所以我们最后打印这类数据。

#### 不同模式应对不同需求

在一个异常处理语句中包裹多行语句显然是有点儿危险的。首先，你没法儿
识别出究竟是哪行代码出现了异常。其次，如果有个页面没有前面的标题内
容，却有“编辑”按钮，那么由于前面已经发生异常，后面的“编辑”按钮
链接就不会出现。但是，这种按照网站上信息出现的可能性高低进行排序的
方法对许多网站都是可行的，偶而会丢失一点儿数据，只要保存详细的日志
就不是什么问题了


你可能还发现在到目前为止所有的例子中，我们都没有“收集”那些“打印”出来的数
据。显然，命令行里显示的数据是很难进一步处理的。我们将在第 5 章继续介绍信息储存
和数据库创建的内容。

> 3.3　通过互联网采集

每次在我做网络数据采集的演讲时，总有人故意问我：“你怎么建一个谷歌网站？”我的
回答通常会包含两点：“首先，你得有几十亿美元能够买得起世界上最大的数据仓库，并
把它们隐秘地放在世界各地。其次，你得写一个网络爬虫。”

谷歌在 1994 年成立的时候，就是两个斯坦福大学的毕业生用一个陈旧的服务器和一个
Python 网络爬虫。现在你应该知道了，你已经正式拥有了成为下一个科技亿万富翁需要的
工具了！

说句实在话，网络爬虫位于许多新式的网络技术领域彼此交叉的中心地带，而且你使用它
们也不需要一个大型数据仓库。要实现任何跨站的数据分析，你只要构建出可以从互联网
上无数的网页里解析和储存数据的爬虫就可以了。

就像之前的例子一样，我们后面要建立的网络爬虫也是顺着链接从一个页面跳到另一个页
面，描绘出一张网络地图。但是这一次，它们不再忽略外链，而是跟着外链跳转。我们想
看看爬虫是不是可以记录我们浏览过的每一个页面上的信息，这将是一个新的挑战。相比
我们之前做的单个域名采集，互联网采集要难得多——不同网站的布局迥然不同。这就意
味着我们必须在要寻找的信息以及查找方式上都极具灵活性。

#### 不知前方水深浅

下一节的代码可以到达互联网的任何位置。如果我们已经掌握了解决“维基
百科六度分隔理论”的方法，那么完全有可能从一个像芝麻街 http://www.sesamestreet.org/ 那样的网站，经过几跳就到达一些非主流网站。

如果读者是小朋友，请在运作代码前咨询一下爸妈。对那些带有敏感题材或
有宗教限制的人来说，某些网站是禁止浏览的，阅读代码示例没问题，但是
运行代码的时候请格外小心。

在你写爬虫随意跟随外链跳转之前，请问自己几个问题
* 我要收集哪些数据？这些数据可以通过采集几个已经确定的网站（永远是最简单的做法）
完成吗？或者我的爬虫需要发现那些我可能不知道的网站吗？
* 当我的爬虫到了某个网站，它是立即顺着下一个出站链接跳到一个新网站，还是在网站
上呆一会儿，深入采集网站的内容？
* 有没有我不想采集的一类网站？我对非英文网站的内容感兴趣吗
* 如果我的网络爬虫引起了某个网站网管的怀疑，我如何避免法律责任？

几个灵活的 Python 函数组合起来就可以实现不同类型的网络爬虫，用不超过 50 行代码就
可轻松地写出来:

    from urllib.request import urlopen
    from bs4 import BeautifulSoup
    import re
    import datetime
    import random

    pages = set()
    random.seed(datetime.datetime.now())

    # 获取页面所有内链的列表
    def getInternalLinks(bsObj, includeUrl):
        internalLinks = []
        # 找出所有以"/"开头的链接
        for link in bsObj.findAll("a", href=re.compile("^(/|.*"+includeUrl+")")):
            if link.attrs['href'] is not None:
                if link.attrs['href'] not in internalLinks:
                    internalLinks.append(link.attrs['href'])
        return internalLinks
    # 获取页面所有外链的列表
    def getExternalLinks(bsObj, excludeUrl):
        externalLinks = []
        # 找出所有以"http"或"www"开头且不包含当前URL的链接
        for link in bsObj.findAll("a",
                                    href=re.compile("^(http|www)((?!"+excludeUrl+").)*$")):
            if link.attrs['href'] is not None:
                if link.attrs['href'] not in externalLinks:
                    externalLinks.append(link.attrs['href'])
        return externalLinks

    def splitAddress(address):
        addressParts = address.replace("http://", "").split("/")
        return addressParts

    def getRandomExternalLink(startingPage):
        html = urlopen(startingPage)
        bsObj = BeautifulSoup(html)
        externalLinks = getExternalLinks(bsObj, splitAddress(startingPage)[0])
        if len(externalLinks) == 0:
            internalLinks = getInternalLinks(startingPage)
            return getNextExternalLink(internalLinks[random.randint(0,
                                                                    len(internalLinks)-1)])
        else:
            return externalLinks[random.randint(0, len(externalLinks)-1)]

    def followExternalOnly(startingSite):
        externalLink = getRandomExternalLink("http://oreilly.com")
        print("随机外链是："+externalLink)
        followExternalOnly(externalLink)

    followExternalOnly("http://oreilly.com")

上面这个程序从 http://oreilly.com 开始，然后随机地从一个外链跳到另一个外链。输出的
结果如下所示：
  
            随机外链是：http://igniteshow.com/
            随机外链是：http://feeds.feedburner.com/oreilly/news
            随机外链是：http://hire.jobvite.com/CompanyJobs/Careers.aspx?c=q319
            随机外链是：http://makerfaire.com/
  

网站首页上并不能保证一直能发现外链。这时为了能够发现外链，就需要用一种类似前面
案例中使用的采集方法，即递归地深入一个网站直到找到一个外链才停止。

#### 不要把示例程序放进产品代码

我想把代码写得更完整，但是写书的时候空间和可读性非常重要，所以书中
的示例程序没有包含真实产品代码中必须有的检查和异常处理。

例如，如果爬虫遇到一个网站里面一个外链都没有（虽然不太可能，但是如
果程序运行的时候够长总会遇到这类情况），这时程序就会一直在这个网站
运行跳不出去，直到递归到达 Python 的限制为止。

在以任何正式的目的运行代码之前，请确认你已经在可能出现问题的地方都
放置了检查语句

把任务分解成像“获取页面上所有外链”这样的小函数是不错的做法，以后可以方便地修
改代码以满足另一个采集任务的需求。例如，如果我们的目标是采集一个网站所有的外
链，并且记录每一个外链，我们可以增加下面的函数：
    
    # 收集网站上发现的所有外链列表
    allExtLinks = set()
    allIntLinks = set()
    def getAllExternalLinks(siteUrl):
        html = urlopen(siteUrl)
        bsObj = BeautifulSoup(html)
        internalLinks = getInternalLinks(bsObj,splitAddress(siteUrl)[0])
        externalLinks = getExternalLinks(bsObj,splitAddress(siteUrl)[0])
        for link in externalLinks:
            if link not in allExtLinks:
                allExtLinks.add(link)
                print(link)
        for link in internalLinks:
            if link not in allIntLinks:
                print("即将获取链接的URL是："+link)
                allIntLinks.add(link)
                getAllExternalLinks(link)

    getAllExternalLinks("http://oreilly.com")
  
这段代码可以看出两个循环——一个是收集内链，一个是收集外链——然后彼此连接起来
工作

<center>处理网页重定向</center>
   
    重定向（redirect）允许一个网页在不同的域名下显示。重定向有两种形式：

    • 服务器端重定向，网页在加载之前先改变了 URL；
    • 客户端重定向，有时你会在网页上看到“10秒钟后页面自动跳转到……”之类的消息，
    表示在跳转到新 URL 之前网页需要加载内容。

    本节处理的是服务器端重定向的内容。更多关于客户端重定向的细节，通常用
    JavaScript 或 HTML 来实现。

    服务器端重定向，你通常不用担心。如果你在用 Python 3.x 版本的 urllib 库，它会自
    动处理重定向。不过要注意，有时候你要采集的页面的 URL 可能并不是你当前所在页
    面的 URL。
 

> 3.4　用Scrapy采集

写网络爬虫的挑战之一是你经常需要不断地重复一些简单任务：找出页面上的所有链接，
区分内链与外链，跳转到新的页面。掌握这些基本模式非常有用，从零开始编写也完全可
行，不过有几个工具可以帮你自动处理这些细节。

Scrapy 就是一个帮你大幅度降低网页链接查找和识别工作复杂度的 Python 库，它可以
让你轻松地采集一个或多个域名的信息。不过目前 Scrapy 仅支持 Python 2.7，还不支持
Python 3.x。

当然在一台机器上同时使用多个版本的 Python 是没有问题的（比如，同时安装 Python 2.7
和 Python 3.4）。如果你既想用 Scrapy 做项目，又想用 Python 3.4 写程序，完全没问题。

Scrapy 网站提供了最新版工具的下载页面（http://scrapy.org/download/），也可以用 pip 等第
三方安装包安装。记住 Python 的版本必须是 2.7（2.6 和 3.x 都不兼容），而且运行所有使
用 Scrapy 的程序也必须在 Python 2.7 环境下。

虽然写 Scrapy 爬虫很简单，但完成一个爬虫还是需要一些设置。如果在当前目录下创建新
的 Scrapy 项目，就执行下面的代码：

    $scrapy startproject wikiSpider
  
wikiSpider 是新项目的名称。在当前目录中会新建一个名称也是 wikiSpider 的项目文件夹。
文件夹的目录结构如下所示：

    • scrapy.cfg
        — wikiSpider
        — __init.py__
        — items.py
        — pipelines.py
        — settings.py
        — spiders
        — __init.py__
 
为 了 创 建 一 个 爬 虫， 我 们 需 要 在 wikiSpider/wikiSpider/spiders/ 文 件 夹 里 增 加 一 个
articleSpider.py 文件。另外，在 items.py 文件中，我们需要定义一个 Article 类。

你的 items.py 文件应该像下面这样（Scrapy 自动生成的注释内容可以保留，当然删除也
可以）：
   
    # -*- coding: utf-8 -*-
    # Define here the models for your scraped items
    #
    # See documentation in:
    # http://doc.scrapy.org/en/latest/topics/items.html

    from scrapy import Item, Field

    class Article(Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
        title = Field()

Scrapy 的每个 Item （条目）对象表示网站上的一个页面。当然，你可以根据需要定义不同
的条目（比如 url 、 content 、 header image 等），但是现在我只演示收集每页的 title 字段
（field）。

在新建的 articleSpider.py 文件里面，写如下代码：
  
    from scrapy.selector import Selector
    from scrapy import Spider
    from wikiSpider.items import Article

    class ArticleSpider(Spider):
        name="article"
        allowed_domains = ["en.wikipedia.org"]
        start_urls = ["http://en.wikipedia.org/wiki/Main_Page",
                                        "http://en.wikipedia.org/wiki/Python_%28programming_language%29"]

    def parse(self, response):
        item = Article()
        title = response.xpath('//h1/text()')[0].extract()
        print("Title is: "+title)
        item['title'] = title
        return item

这个类的名称（ ArticleSpider ）与爬虫文件的名称（wikiSpider）是不同的，这个类只是
在 wikiSpider 目录里的一员，仅仅用于维基词条页面的采集。对一些信息类型较多的大网
站，你可能会为每种信息（如博客的博文、图书出版发行信息、专栏文章等）设置独立的
Scrapy 条目，每个条目都有不同的字段，但是所有条目都在同一个 Scrapy 项目里运行。

你可以在 wikiSpider 主目录中用如下命令运行 ArticleSpider ：
   
    $ scrapy crawl article
     
这行命令会用条目名称 article 来调用爬虫（不是类名，也不是文件名，而是由
ArticleSpider 的 name = "article" 决定的）。

陆续出现的调试信息中应该会这两行结果：
    
    Title is: Main Page
    Title is: Python (programming language)
   
这个爬虫先进入 start_urls 里面的两个页面，收集信息，然后停止。虽然这个爬虫很简
单，但是如果你有许多 URL 需要采集，Scrapy 这种用法会非常适合。为了让爬虫更加完
善，你需要定义一些规则让 Scrapy 可以在每个页面查找 URL 链接：
 
    from scrapy.contrib.spiders import CrawlSpider, Rule
    from wikiSpider.items import Article
    from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor

    class ArticleSpider(CrawlSpider):
        name="article"
        allowed_domains = ["en.wikipedia.org"]
        start_urls = ["http://en.wikipedia.org/wiki/Python_
                                %28programming_language%29"]
        rules = [Rule(SgmlLinkExtractor(allow=('(/wiki/)((?!:).)*$'),),
                                                    callback="parse_item", follow=True)]

    def parse_item(self, response):
        item = Article()
        title = response.xpath('//h1/text()')[0].extract()
        print("Title is: "+title)
        item['title'] = title
        return item
 
虽然这个爬虫和前面那个爬虫的启动命令一样，但是如果你不用 Ctrl+C 中止程序，它是不
会停止的（很长时间也不会停止）。

#### Scrapy 日志处理

Scrapy 生成的调试信息非常有用，但是通常太罗嗦。你可以在 Scrapy 项目中
的 setting.py 文件中设置日志显示层级：

    LOG_LEVEL = 'ERROR'

Scrapy 日志有五种层级，按照范围递增顺序排列如下：
* CRITICAL
* ERROR
* WARNING
* DEBUG
* INFO

如果日志层级设置为 ERROR ，那么只有 CRITICAL 和 ERROR 日志会显示出来。
如果日志层级设置为 INFO ，那么所有信息都会显示出来，其他同理。

日志不仅可以显示在终端，也可以通过下面命令输出到一个独立的文件中：
  
    $ scrapy crawl article -s LOG_FILE=wiki.log
 
如果目录中没有 wiki.log，那么运行程序会创建一个新文件，然后把所有的
日志都保存到里面。如果已经存在，会在原文后面加入新的日志内容。


Scrapy 用 Item 对象决定要从它浏览的页面中提取哪些信息。Scrapy 支持用不同的输出格
式来保存这些信息，比如 CSV、JSON 或 XML 文件格式，对应命令如下所示：
   
    $ scrapy crawl article -o articles.csv -t csv
    $ scrapy crawl article -o articles.json -t json
    $ scrapy crawl article -o articles.xml -t xml
   
当然，你也可以自定义 Item 对象，把结果写入你需要的一个文件或数据库中，只要在爬虫
的 parse 部分增加相应的代码即可。

Scrapy 是处理网络数据采集相关问题的利器。它可以自动收集所有 URL，然后和指定的规
则进行比较；确保所有的 URL 是唯一的；根据需求对相关的 URL 进行标准化；以及到更
深层的页面中递归查找。

尽管这点内容算只能是碰到了 Scrapy 强大功能的一角，但我依然鼓励你去学习 Scrapy 文
档（http://doc.scrapy.org/en/latest/）和其他在线的学习资源。Scrapy 的内容非常丰富，具有
很多特性。如果那些你想用 Scrapy 做的事情在这里没有提到，那么 Scrapy 很可能有一种
（或几种）方法可以满足你的需求


           
           
           `,
        };
      }
    render() {
        return (
            <div >
                {/* 111111111111111
                <Py112 /> */}
                <MarkdownPreview value={this.state.value}/>
            </div>
        )
    }
}

export default Py13